Repository navigation
feat(local-model): pick the coding agent per session, with Pi and DeepSeek Harness, and install a missing one - #806
Merged
Merged
Conversation
The local model window could hold one model. A second install was refused (409 already-installed), and a model a Remove kept could never be freed from the window again. The store can now hold several models, and exactly one of them is the model in use: the one the endpoint record lists, the runtime is started for and a session may be launched on. Server - LocalModelStateFile.library lists every model that passed the check (id, label, context, digest, size, coding agent). The service writes the entry when verify passes; the reducer carries it and remove drops it. An installed file of an earlier build counts its one model. - POST /api/local-model/install is allowed from installed: it adds a model, and the earlier one stays. A replaced job no longer deletes a model of the library, only an unchecked or partial download. - POST /api/local-model/use switches to a checked model: a job of the two steps verify and connect. The stored digest is compared before anything is loaded (model-changed otherwise). Nothing is downloaded. - DELETE /api/local-model/models deletes one model by its name, through the runtime's own delete, never by a path, never the model in use. - Adding and switching answer 409 sessions-open while a live session uses the endpoint. Until such a job ends installed, a session start is refused with the new code job-unfinished. - The view carries library (the state's entries joined with what the running runtime lists: ready, changed, missing, unverified) and store (the size of the blob folder). Both are cached and refreshed in the background, never read on the one-second poll. - With nothing installed, remove is offered while the store still holds models an earlier Remove kept. Window - The installed screen lists the models on this Mac with the context each was checked with, its state and its size on disk, with Use and Delete per row (Delete asks once, in the row) and Add a model. - In the chooser a model that is on this Mac already says so and is switched to without a download or a confirm screen. - A paused or failed job that has a checked model beside it shows the way back to that model. Every route is admin only in multi-user mode, like the existing ones. Not run on a Mac: the tests use fakes and the fake runtime, and the listing, store size and delete were checked against a real Ollama 0.13.0 on Linux, not the pinned 0.40.15 build for macOS. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The guide, the API reference, the invariants and the two CLAUDE files describe the library with one model in use: the state file's list, the three verbs and their refusals, the two cached listings of the view, and the window's list with its row actions. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
… the Stop A job started from a model the person had stopped runs the runtime, and the model it ends on is in use and running. The stored "stopped" flag stayed set, so that model would not have started with the server again. Both verbs now set it back once their job started, the way Start does. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A pi session on a custom endpoint got a model list entry with only the model id. pi then takes the model for a 128,000-token one (its footer reads "/128k") and asks for 16,384 reply tokens, so on a 65,536 or 32,768 window it never compacts before the server's real window is full. The entry now carries `contextWindow` (the full window) and `maxTokens` (a quarter of it, 8,192 at most) whenever the endpoint's context length is known: the same two figures the DeepSeek Harness patch already carries, now from one helper. An unknown length writes neither, as before. Measured on pi 1.5.2: with both fields `pi --list-models` shows the window, and the request carries `max_tokens: 8192`. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…d DeepSeek Harness
An installed model knew one coding agent: the one it was installed with.
The launch check never asked about the agent, only about the endpoint
and the model, so nothing stopped a second one. What was missing is the
list, and a way to get an agent that is not on the Mac yet.
The install view gains `harnesses`: every catalogue harness the model in
use can be run with, each with how it stands (`ready`, `installable`,
`installing`, `setup-needed`), the literal install command, a readiness
mark for scripts, and the actions the server accepts for it. `cli` stays
the default agent, and names one that can be started whenever one can,
so a client that reads only `cli` never starts an agent that is gone.
`POST /api/local-model/agent { cli }` installs one more agent beside an
installed model. It is not a job and not an event of the reducer: the
state stays `installed` whatever the installer does, so a failed npm
install never blocks a session on a model that works. Same install slot
and same compiled stock command as the `agent` step, one at a time, the
outcome in memory only.
The catalogue:
- `pi` is no longer gated. Its two open questions were answered by a run
against a real model (the `name:tag` id reaches the runtime unchanged;
its first request is 2,096 tokens), and Codeman now passes it the
window. Its minimum is 65536 like the others: nothing below was run,
and no model row offers less, so no Mac's pick changes.
- `deepseek` is new. It is a launcher: its install command leaves it
with nothing to launch, so it is never reported installable (read off
`discovery.launcherProfile`, not the id) and a Mac is offered it only
where it was already set up. Its tool round trip was seen on a
scripted endpoint only; the header says how to gate it again.
- a row may carry `readyMark`; `opencode` has the measured one.
A switch no longer refuses for the agent a model was checked with when
another agent that can run it is on the Mac.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…ne that is missing The installed screen gets one more block, "Start a session with": a chip per coding agent the model in use can be run with, as a radio group, the server's default selected. A chip that is not on the Mac says so. The main button follows the selected chip: - an agent that can be started: "Start a session", with that agent; - an agent that is not on the Mac: its literal install command above the button, and "Install <agent>". The click is the consent; the server runs its own stock command, and the window shows "Installing" on the chip while the model stays ready and another agent can be started; - an agent that needs a setup of its own (DeepSeek Harness without a terminal profile): one line that says where, nothing to press. A failed install shows the server's sentence under the chip with the installer's last words folded away; the model's own headline is untouched. On a stopped model Start stays the one main button. The choice is remembered on the device. A server that lists no agents gets the screen it had. Rendered in Chromium at 1440, 390 and 320 px on a dark and a light skin: the chips wrap, nothing leaves the window. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…ed with, and the agent install - docs/local-models.md: the four agents with their first request, whether the window installs them and what was seen where (a real model, a scripted endpoint, Linux, not the pinned runtime); what differs for Pi and for DeepSeek Harness; installing one more agent beside a model. - docs/api-reference.md: `POST /api/local-model/agent`, `install.harnesses`, the `install-agent` action, and `install.cli` as the default agent. - docs/architecture-invariants.md, CLAUDE.md, src/web/CLAUDE.md: the rules: the agent is chosen per session, the agent install is not a job, a launcher is never installable, a readiness mark is a measured one, and the window's one builder for the agent chips. - docs/custom-model-endpoints.md: Pi's context window. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This was referenced Nov 11, 2026
A second scripted run, on dsh 0.2.2-rc.1 with a freshly installed dsh-TUI 0.19.0 under a scratch home: the model name reaches the endpoint unchanged, the first request is 6,983 tokens with 27 tools, and a tool round trip completes. Two things that version does differently are written down where a reader of the harness table looks: a setup guide opens in every session until a person has finished it once (and its cursor is the readiness mark), and its transcript file was renamed, so `last-response` is empty for it. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
… session" Selecting a chip stored it for the device at once. Someone who clicked "DeepSeek" (Needs setup) or "Pi" (Not installed) just to see what it says then got a window that reopened on that chip: "Install Pi", or no main button at all, with "Start a session" out of sight. The device now remembers the agent a session was STARTED with, an opening of the window selects it only while it can be started, and a selection made in one opening does not carry into the next. Otherwise the server's default agent is selected, as before. Also: a real-browser test for the chips (they wrap inside the window at 1440, 390 and 320 px, the install command included, one tab stop), and one sentence in the API reference: a readiness mark says the composer is there, not that nothing else is (DeepSeek Harness's setup guide draws the same glyph). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…his model" refusal
Seen on a MacBook (M5 Pro, 2026-11-12): with a worker of the codeman skill open on the
local model, Stop, Remove and a switch of models answered
Sessions are still using this model: . Close them first.
A session an agent starts has no name of its own (a quick-start without sessionName), and
the refusal listed session.name. The tab of such a session shows the last folder of its
working directory, else the first eight characters of its id, so the sentence now uses the
same label (sessionLabel() beside sessionNamesUsingEndpoint()).
The sentence has shipped since 1.50.0 for Stop and Remove. This branch adds it to Use and
to the install of another model, which is where a person with local workers open meets it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…-button screen Seen on a MacBook (M5 Pro, 2026-11-12): pressing Use on a model in "Models on this Mac" ran the switch and then showed the screen that ends an install, "X is ready on this Mac" with Start a session alone. The list was gone, so deleting the model just switched away from meant closing the window and opening it again. That screen is right after a download: the person installed a model and wants a session. A switch is pressed IN the list by someone who is tidying it. A switch is a job without a `model` step, so the window now tells the two apart from the job it just watched and ends a switch on the whole installed screen: the list with "In use" on the other row, Add a model, Stop and Remove. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…-choice # Conflicts: # src/web/CLAUDE.md
Ark0N
pushed a commit
that referenced
this pull request
Nov 12, 2026
…arness, and install a missing one Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
"Run a local model" is the window that installs a coding model on the Mac the Codeman server runs on. A model is driven by a coding agent CLI (a "harness"). Until now an installed model knew exactly one agent, the one it was installed with, and only OpenCode and Claude Code were offered.
This PR makes the agent a per-session choice and adds two agents:
The four agents, and what each rests on
How to read the last column. The runs were made on 2026-11-10 on Linux, through Codeman's own custom model launch:
None of it ran on a Mac or on the pinned runtime (Ollama 0.40.15). Every agent sent the
name:tagmodel name unchanged.Decisions a maintainer should look at
docs/custom-model-endpoints.md). It is ranked last and never installed from here, so only a Mac that already has it set up sees it as startable. To gate it again:gated: trueon its row insrc/config/local-model-catalog.ts, one line.Two things today's DeepSeek Harness (0.2.2-rc.1 with dsh-TUI 0.19.0) does that are worth knowing, both independent of this PR:
session.v4.jsonl.zstd), so Codeman's response viewer andlast-responseare empty for a dsh session on that version. That is a bug of its own, for every dsh session, and is not fixed here.dshis a launcher: its install command installs the launcher, and a session also needs a terminal profile, which is third-party code behind its own consent in the Run menu. So it shows "Needs setup" until the Run menu can start it. The rule reads the registry entry'sdiscovery.launcherProfile, not the CLI id. Folding the profile install into this window is possible and is a decision of its own.PI_OFFLINE=1skips it);A person in a tab can answer the question. A script cannot, which is why the follow-up for the codeman skill starts Pi with
--no-approve.How it is built
GET /api/local-modelgainsinstall.harnesses[]:{ cli, cliLabel, default, status, installCommand?, slowFirstTurn?, readyMark?, error?, actions }.statusisready,installable,installingorsetup-needed.readyMarkis a literal the agent's composer draws once it takes input, for a script that waits before its first prompt; only a measured mark is listed (OpenCode┃, DeepSeek Harness❯, and Claude Code'sshift+tabfrom its registry entry; Pi has none on purpose, its composer is painted before it accepts Enter).install.clistays the default agent for a client that reads only that field (the codeman skill shipped in 1.50.0 does). It now names an agent that can be started whenever one can, never one that was uninstalled.POST /api/local-model/agent { cli }installs one more agent beside an installed model. It is deliberately not a job of the install state machine: a job would put a working model intofailedon annpmerror and refuse every session until someone cleared it. The state staysinstalled, the installer runs in the background through the same install slot and the same compiled stock command as the install's own agent step, one at a time, audited (cli_install,via: "local-model"), and its outcome is kept in memory only. The request carries a CLI id, never a command.piis no longer gated,deepseekis a new row, a row may carryreadyMark, and every row asks for 65,536 tokens (nothing below was run, and no model row offers less, so no Mac's recommended model changes). The catalogue header has a table of what each row rests on.The Pi fix (commit
a5a1b6f9, independent of the rest)A Pi session on any custom endpoint got a model list entry with only the model id. Pi then takes the model for a 128,000-token one and asks for 16,384 reply tokens, so on a 65,536 window it never compacts before the server's real window is full. The entry now carries
contextWindowandmaxTokenswhen the endpoint's context length is known, the same two figures the DeepSeek Harness recipe already had. Measured on pi 1.5.2:pi --list-modelsshows the window and the request carriesmax_tokens: 8192.API (experimental routes, outside the stable
/api/v1contract)POST /api/local-model/agent{ cli }install.harnesses, or an agent one command does not make startable; 409job-running(a job or another agent install runs),wrong-state(nothing is installed)Admin only in multi-user mode, checked before the body is parsed, like every other route of the feature.
Testing
test/local-model-*andtest/routes/local-model-*file, the custom model tests, the i18n harvest, both CLI id-branching guards and the HTTP contract test.npm run test:browser -- test/local-model-ui.browser.test.ts(real Chromium, not part of the gate): 11 passed, one of them new (the chips wrap inside the window at 1440, 390 and 320 px, the install command included, with one tab stop). The screen was also looked at on a dark and a light skin with a scratch script.Run on a real Mac (2026-11-12)
A MacBook Pro (M5 Pro, 48 GB, macOS 27.2) with the pinned runtime, Ollama 0.40.15. Its Codeman ran the tree of this stack (#803 to #808) merged onto 1.50.1. The window was driven by real clicks, from a Chromium on another machine through an SSH forward to the Mac's own server, so every state on screen was the Mac's.
npm install -g --ignore-scripts @earendil-works/pi-coding-agentran for about 5 seconds. The chip went from "Installing" to startable, the install state stayedinstalledthe whole time, and the audit log holds thecli_installentry withvia: "local-model".fdandripgrepat the start of every session there too. It did not ask about folder trust, because no folder above the project holds agent skills on that Mac.max_completion_tokens: asked for 64 tokens, Ollama 0.40.15 returned 1,235. Withmax_tokens: 64it returned 64. DeepSeek Harness caps its requests with the former, so its small side requests run uncapped on this runtime. One more reason its row is ranked last.Not run on the Mac: DeepSeek Harness (it is not set up there), and Claude Code ON the local model (Claude Code ran there as the cloud lead of the skill test in #807).
Not proven, stated plainly
Files
Server:
src/local-model/{install-job,harness-facts}.ts,src/types/{local-model,local-model-install}.ts,src/web/routes/local-model-routes.ts,src/web/schemas.ts,src/config/local-model-catalog.ts,src/custom-model-injection.ts. Window:src/web/public/{local-model-ui.js,styles.css,i18n.js}. Tests:test/local-model-{install-job,install-service,harness-facts,catalog,recommend,ui}.test.ts,test/routes/local-model-routes.test.ts,test/custom-model-injection.test.ts. Docs:CLAUDE.md,src/web/CLAUDE.md,docs/{local-models,api-reference,architecture-invariants,custom-model-endpoints}.md.